/** * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.commons.file.protocol.local; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.util.StringTokenizer; import java.util.Vector; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.FileOperation; import com.mucommander.commons.file.FilePermissions; import com.mucommander.commons.file.FileURL; import com.mucommander.commons.file.GroupedPermissionBits; import com.mucommander.commons.file.IndividualPermissionBits; import com.mucommander.commons.file.MacOsSystemFolder; import com.mucommander.commons.file.PermissionAccess; import com.mucommander.commons.file.PermissionBits; import com.mucommander.commons.file.PermissionType; import com.mucommander.commons.file.UnsupportedFileOperation; import com.mucommander.commons.file.UnsupportedFileOperationException; import com.mucommander.commons.file.filter.FilenameFilter; import com.mucommander.commons.file.protocol.FileProtocols; import com.mucommander.commons.file.protocol.ProtocolFile; import com.mucommander.commons.file.util.Kernel32; import com.mucommander.commons.file.util.Kernel32API; import com.mucommander.commons.file.util.PathUtils; import com.mucommander.commons.io.BufferPool; import com.mucommander.commons.io.FilteredOutputStream; import com.mucommander.commons.io.RandomAccessInputStream; import com.mucommander.commons.io.RandomAccessOutputStream; import com.mucommander.commons.runtime.JavaVersion; import com.mucommander.commons.runtime.OsFamily; import com.mucommander.commons.runtime.OsVersion; import com.sun.jna.ptr.LongByReference; /** * LocalFile provides access to files located on a locally-mounted filesystem. * Note that despite the class' name, LocalFile instances may indifferently be residing on a local hard drive, * or on a remote server mounted locally by the operating system. * * <p>The associated {@link FileURL} scheme is {@link FileProtocols#FILE}. The host part should be {@link FileURL#LOCALHOST}, * except for Windows UNC URLs (see below). Native path separators ('/' or '\\' depending on the OS) can be used * in the path part. * * <p>Here are a few examples of valid local file URLs: * <code> * file://localhost/C:\winnt\system32\<br> * file://localhost/usr/bin/gcc<br> * file://localhost/~<br> * file://home/maxence/..<br> * </code> * * <p>Windows UNC paths can be represented as FileURL instances, using the host part of the URL. The URL format for * those is the following:<br> * <code>file:\\server\share</code> .<br> * * <p>Under Windows, LocalFile will translate those URLs back into a UNC path. For example, a LocalFile created with the * <code>file://garfield/stuff</code> FileURL will have the <code>getAbsolutePath()</code> method return * <code>\\garfield\stuff</code>. Note that this UNC path translation doesn't happen on OSes other than Windows, which * would not be able to handle the path. * * <p>Access to local files is provided by the <code>java.io</code> API, {@link #getUnderlyingFileObject()} allows * to retrieve an <code>java.io.File</code> instance corresponding to this LocalFile. * * @author Maxence Bernard */ public class LocalFile extends ProtocolFile { private static final Logger LOGGER = LoggerFactory.getLogger(LocalFile.class); protected File file; private FilePermissions permissions; /** Absolute file path, free of trailing separator */ protected String absPath; /** Caches the parent folder, initially null until getParent() gets called */ protected AbstractFile parent; /** Indicates whether the parent folder instance has been retrieved and cached or not (parent can be null) */ protected boolean parentValueSet; /** Underlying local filesystem's path separator: "/" under UNIX systems, "\" under Windows and OS/2 */ public final static String SEPARATOR = File.separator; /** Are we running Windows ? */ private final static boolean IS_WINDOWS = OsFamily.WINDOWS.isCurrent(); /** True if the underlying local filesystem uses drives assigned to letters (e.g. A:\, C:\, ...) instead * of having single a root folder '/' */ public final static boolean USES_ROOT_DRIVES = IS_WINDOWS || OsFamily.OS_2.isCurrent(); /** Pattern matching Windows-like drives' root, e.g. C:\ */ final static Pattern DRIVE_ROOT_PATTERN = Pattern.compile("^[a-zA-Z]{1}[:]{1}[\\\\]{1}"); // Permissions can only be changed under Java 1.6 and up and are limited to 'user' access. // Note: 'read' and 'execute' permissions have no meaning under Windows (files are either read-only or // read-write) and as such can't be changed. /** Changeable permissions mask for Java 1.6 and up, on OSes other than Windows */ private static PermissionBits CHANGEABLE_PERMISSIONS_JAVA_1_6_NON_WINDOWS = new GroupedPermissionBits(448); // rwx------ (700 octal) /** Changeable permissions mask for Java 1.6 and up, on Windows OS (any version) */ private static PermissionBits CHANGEABLE_PERMISSIONS_JAVA_1_6_WINDOWS = new GroupedPermissionBits(128); // -w------- (200 octal) /** Changeable permissions mask for Java 1.5 or below */ private static PermissionBits CHANGEABLE_PERMISSIONS_JAVA_1_5 = PermissionBits.EMPTY_PERMISSION_BITS; // --------- (0) /** Bit mask that indicates which permissions can be changed */ private final static PermissionBits CHANGEABLE_PERMISSIONS = JavaVersion.JAVA_1_6.isCurrentOrHigher() ?(IS_WINDOWS?CHANGEABLE_PERMISSIONS_JAVA_1_6_WINDOWS:CHANGEABLE_PERMISSIONS_JAVA_1_6_NON_WINDOWS) : CHANGEABLE_PERMISSIONS_JAVA_1_5; /** * List of known UNIX filesystems. */ public static final String[] KNOWN_UNIX_FS = { "adfs", "affs", "autofs", "cifs", "coda", "cramfs", "debugfs", "efs", "ext2", "ext3", "fuseblk", "hfs", "hfsplus", "hpfs", "iso9660", "jfs", "minix", "msdos", "ncpfs", "nfs", "nfs4", "ntfs", "qnx4", "reiserfs", "smbfs", "udf", "ufs", "usbfs", "vfat", "xfs" }; static { // Prevents Windows from poping up a message box when it cannot find a file. Those message box are triggered by // java.io.File methods when operating on removable drives such as floppy or CD-ROM drives which have no disk // inserted. // This has been fixed in Java 1.6 b55 but this fixes previous versions of Java. // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4089199 if(IS_WINDOWS && Kernel32.isAvailable()) Kernel32.getInstance().SetErrorMode(Kernel32API.SEM_NOOPENFILEERRORBOX|Kernel32API.SEM_FAILCRITICALERRORS); } /** * Creates a new instance of LocalFile and a corresponding {@link File} instance. */ protected LocalFile(FileURL fileURL) throws IOException { this(fileURL, null); } /** * Creates a new instance of LocalFile, using the given {@link File} if not <code>null</code>, creating a new * {@link File} instance otherwise. */ protected LocalFile(FileURL fileURL, File file) throws IOException { super(fileURL); if(file==null) { String path = fileURL.getPath(); // Remove the leading '/' for Windows-like paths if(USES_ROOT_DRIVES) path = path.substring(1, path.length()); // Create the java.io.File instance and throw an exception if the path is not absolute. file = new File(path); if(!file.isAbsolute()) throw new IOException(); absPath = file.getAbsolutePath(); // Remove the trailing separator if present if(absPath.endsWith(SEPARATOR)) absPath = absPath.substring(0, absPath.length()-1); } // the java.io.File instance was created by ls(), no need to re-create it or call the costly File#getAbsolutePath() else { this.absPath = fileURL.getPath(); // Remove the leading '/' for Windows-like paths if(USES_ROOT_DRIVES) absPath = absPath.substring(1, absPath.length()); } this.file = file; this.permissions = new LocalFilePermissions(file); } //////////////////////////////// // LocalFile-specific methods // //////////////////////////////// /** * Returns the user home folder. Most if not all OSes have one, but in the unlikely event that the OS doesn't have * one or that the folder cannot be resolved, <code>null</code> will be returned. * * @return the user home folder */ public static AbstractFile getUserHome() { String userHomePath = System.getProperty("user.home"); if(userHomePath==null) return null; return FileFactory.getFile(userHomePath); } /** * Returns the total and free space on the volume where this file resides. * * <p>Using this method to retrieve both free space and volume space is more efficient than calling * {@link #getFreeSpace()} and {@link #getTotalSpace()} separately -- the underlying method retrieving both * attributes at the same time.</p> * * @return a {totalSpace, freeSpace} long array, both values can be null if the information could not be retrieved * @throws IOException if an I/O error occurred */ public long[] getVolumeInfo() throws IOException { // Under Java 1.6 and up, use the (new) java.io.File methods if(JavaVersion.JAVA_1_6.isCurrentOrHigher()) { return new long[] { getTotalSpace(), getFreeSpace() }; } // Under Java 1.5 or lower, use native methods return getNativeVolumeInfo(); } /** * Uses platform dependent functions to retrieve the total and free space on the volume where this file resides. * * @return a {totalSpace, freeSpace} long array, both values can be <code>null</code> if the information could not * be retrieved. * @throws IOException if an I/O error occurred */ protected long[] getNativeVolumeInfo() throws IOException { BufferedReader br = null; String absPath = getAbsolutePath(); long dfInfo[] = new long[]{-1, -1}; try { // OS is Windows if(IS_WINDOWS) { // Use the Kernel32 DLL if it is available if(Kernel32.isAvailable()) { // Retrieves the total and free space information using the GetDiskFreeSpaceEx function of the // Kernel32 API. LongByReference totalSpaceLBR = new LongByReference(); LongByReference freeSpaceLBR = new LongByReference(); if(Kernel32.getInstance().GetDiskFreeSpaceEx(absPath, null, totalSpaceLBR, freeSpaceLBR)) { dfInfo[0] = totalSpaceLBR.getValue(); dfInfo[1] = freeSpaceLBR.getValue(); } else { LOGGER.warn("Call to GetDiskFreeSpaceEx failed, absPath={}", absPath); } } // Otherwise, parse the output of 'dir "filePath"' command to retrieve free space information, if // running Window NT or higher. // Note: no command invocation under Windows 95/98/Me, because it causes a shell window to // appear briefly every time this method is called (See ticket #63). else if(OsVersion.WINDOWS_NT.isCurrentOrHigher()) { // 'dir' command returns free space on the last line Process process = Runtime.getRuntime().exec( (OsVersion.getCurrent().compareTo(OsVersion.WINDOWS_NT)>=0 ? "cmd /c" : "command.com /c") + " dir \""+absPath+"\""); // Check that the process was correctly started if(process!=null) { br = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; String lastLine = null; // Retrieves last line of dir while((line=br.readLine())!=null) { if(!line.trim().equals("")) lastLine = line; } // Last dir line may look like something this (might vary depending on system's language, below in French): // 6 Rep(s) 14 767 521 792 octets libres if(lastLine!=null) { StringTokenizer st = new StringTokenizer(lastLine, " \t\n\r\f,."); // Discard first token st.nextToken(); // Concatenates as many contiguous groups of numbers String token; String freeSpace = ""; while(st.hasMoreTokens()) { token = st.nextToken(); char c = token.charAt(0); if(c>='0' && c<='9') freeSpace += token; else if(!freeSpace.equals("")) break; } dfInfo[1] = Long.parseLong(freeSpace); } } } } else if(OsFamily.getCurrent().isUnixBased()) { // Parses the output of 'df -P -k "filePath"' command on UNIX-based systems to retrieve free and total space information // 'df -P -k' returns totals in block of 1K = 1024 bytes, -P uses the POSIX output format, ensures that line won't break Process process = Runtime.getRuntime().exec(new String[]{"df", "-P", "-k", absPath}, null, file); // Check that the process was correctly started if(process!=null) { br = new BufferedReader(new InputStreamReader(process.getInputStream())); // Discard the first line ("Filesystem 1K-blocks Used Avail Capacity Mounted on"); br.readLine(); String line = br.readLine(); // Sample lines: // /dev/disk0s2 116538416 109846712 6179704 95% / // automount -fstab [202] 0 0 0 100% /automount/Servers // /dev/disk2s2 2520 1548 972 61% /Volumes/muCommander 0.8 // We're interested in the '1K-blocks' and 'Avail' fields (only). // The 'Filesystem' and 'Mounted On' fields can contain spaces (e.g. 'automount -fstab [202]' and // '/Volumes/muCommander 0.8' resp.) and therefore be made of several tokens. A stable way to // determine the position of the fields we're interested in is to look for the last token that // starts with a '/' character which should necessarily correspond to the first token of the // 'Mounted on' field. The '1K-blocks' and 'Avail' fields are 4 and 2 tokens away from it // respectively. // Start by tokenizing the whole line Vector<String> tokenV = new Vector<String>(); if(line!=null) { StringTokenizer st = new StringTokenizer(line); while(st.hasMoreTokens()) tokenV.add(st.nextToken()); } int nbTokens = tokenV.size(); if(nbTokens<6) { // This shouldn't normally happen LOGGER.warn("Failed to parse output of df -k {} line={}", absPath, line); return dfInfo; } // Find the last token starting with '/' int pos = nbTokens-1; while(!tokenV.elementAt(pos).startsWith("/")) { if(pos==0) { // This shouldn't normally happen LOGGER.warn("Failed to parse output of df -k {} line={}", absPath, line); return dfInfo; } --pos; } // '1-blocks' field (total space) dfInfo[0] = Long.parseLong(tokenV.elementAt(pos-4)) * 1024; // 'Avail' field (free space) dfInfo[1] = Long.parseLong(tokenV.elementAt(pos-2)) * 1024; } // // Retrieves the total and free space information using the POSIX statvfs function // POSIX.STATVFSSTRUCT struct = new POSIX.STATVFSSTRUCT(); // if(POSIX.INSTANCE.statvfs(absPath, struct)==0) { // dfInfo[0] = struct.f_blocks * (long)struct.f_frsize; // dfInfo[1] = struct.f_bfree * (long)struct.f_frsize; // } } } finally { if(br!=null) try { br.close(); } catch(IOException e) {} } return dfInfo; } /** * Attemps to detect if this file is the root of a removable media drive (floppy, CD, DVD, USB drive...). * This method produces accurate results only under Windows. * * @return <code>true</code> if this file is the root of a removable media drive (floppy, CD, DVD, USB drive...). */ public boolean guessRemovableDrive() { if(IS_WINDOWS && Kernel32.isAvailable()) { int driveType = Kernel32.getInstance().GetDriveType(getAbsolutePath(true)); if(driveType!=Kernel32API.DRIVE_UNKNOWN) return driveType==Kernel32API.DRIVE_REMOVABLE || driveType==Kernel32API.DRIVE_CDROM; } // For other OS that have root drives (OS/2), a weak way to characterize removable drives is by checking if the // corresponding root folder is read-only. return hasRootDrives() && isRoot() && !file.canWrite(); } /** * Returns <code>true</code> if the underlying local filesystem uses drives assigned to letters (e.g. A:\, C:\, ...) * instead of having a single root folder '/' under which mount points are attached. * This is <code>true</code> for the following platforms: * <ul> * <li>Windows</li> * <li>OS/2</li> * <li>Any other platform that has '\' for a path separator</li> * </ul> * * @return <code>true</code> if the underlying local filesystem uses drives assigned to letters */ public static boolean hasRootDrives() { return IS_WINDOWS || OsFamily.OS_2.isCurrent() || "\\".equals(SEPARATOR); } /** * Resolves and returns all local volumes: * <ul> * <li>On UNIX-based OSes, these are the mount points declared in <code>/etc/ftab</code>.</li> * <li>On the Windows platform, these are the drives displayed in Explorer. Some of the returned volumes may * correspond to removable drives and thus may not always be available -- if they aren't, {@link #exists()} will * return <code>false</code>.</li> * </ul> * <p> * The return list of volumes is purposively not cached so that new volumes will be returned as soon as they are * mounted. * </p> * * @return all local volumes */ public static AbstractFile[] getVolumes() { Vector<AbstractFile> volumesV = new Vector<AbstractFile>(); // Add Mac OS X's /Volumes subfolders and not file roots ('/') since Volumes already contains a named link // (like 'Hard drive' or whatever silly name the user gave his primary hard disk) to / if(OsFamily.MAC_OS_X.isCurrent()) { addMacOSXVolumes(volumesV); } else { // Add java.io.File's root folders addJavaIoFileRoots(volumesV); // Add /proc/mounts folders under UNIX-based systems. if(OsFamily.getCurrent().isUnixBased()) addMountEntries(volumesV); } // Add home folder, if it is not already present in the list AbstractFile homeFolder = getUserHome(); if(!(homeFolder==null || volumesV.contains(homeFolder))) volumesV.add(homeFolder); AbstractFile volumes[] = new AbstractFile[volumesV.size()]; volumesV.toArray(volumes); return volumes; } //////////////////// // Helper methods // //////////////////// /** * Resolves the root folders returned by {@link File#listRoots()} and adds them to the given <code>Vector</code>. * * @param v the <code>Vector</code> to add root folders to */ private static void addJavaIoFileRoots(Vector<AbstractFile> v) { // Warning : No file operation should be performed on the resolved folders as under Win32, this would cause a // dialog to appear for removable drives such as A:\ if no disk is present. File fileRoots[] = File.listRoots(); int nbFolders = fileRoots.length; for(int i=0; i<nbFolders; i++) try { v.add(FileFactory.getFile(fileRoots[i].getAbsolutePath(), true)); } catch(IOException e) {} } /** * Parses <code>/proc/mounts</code> kernel virtual file, resolves all the mount points that look like regular * filesystems it contains and adds them to the given <code>Vector</code>. * * @param v the <code>Vector</code> to add mount points to */ private static void addMountEntries(Vector<AbstractFile> v) { BufferedReader br; br = null; try { br = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/mounts"))); StringTokenizer st; String line; AbstractFile file; String mountPoint, fsType; boolean knownFS; // read each line in file and parse it while ((line=br.readLine())!=null) { line = line.trim(); // split line into tokens separated by " \t\n\r\f" // tokens are: device, mount_point, fs_type, attributes, fs_freq, fs_passno st = new StringTokenizer(line); st.nextToken(); mountPoint = st.nextToken().replace("\\040", " "); fsType = st.nextToken(); knownFS = false; for (String fs : KNOWN_UNIX_FS) { if (fs.equals(fsType)) { // this is really known physical FS knownFS = true; break; } } if (knownFS) { file = FileFactory.getFile(mountPoint); if(file!=null && !v.contains(file)) v.add(file); } } } catch(Exception e) { LOGGER.warn("Error parsing /proc/mounts entries", e); } finally { if(br != null) { try { br.close(); } catch(IOException e) {} } } } /** * Adds all <code>/Volumes</code> subfolders to the given <code>Vector</code>. * * @param v the <code>Vector</code> to add the volumes to */ private static void addMacOSXVolumes(Vector<AbstractFile> v) { // /Volumes not resolved for some reason, giving up AbstractFile volumesFolder = FileFactory.getFile("/Volumes"); if(volumesFolder==null) return; // Adds subfolders try { AbstractFile volumesFiles[] = volumesFolder.ls(); int nbFiles = volumesFiles.length; AbstractFile folder; for(int i=0; i<nbFiles; i++) if((folder=volumesFiles[i]).isDirectory()) { // The primary hard drive (the one corresponding to '/') is listed under Volumes and should be // returned as the first volume if(folder.getCanonicalPath().equals("/")) v.insertElementAt(folder, 0); else v.add(folder); } } catch(IOException e) { LOGGER.warn("Can't get /Volumes subfolders", e); } } ///////////////////////////////// // AbstractFile implementation // ///////////////////////////////// /** * Returns a <code>java.io.File</code> instance corresponding to this file. */ @Override public Object getUnderlyingFileObject() { return file; } @Override public boolean isSymlink() { // At the moment symlinks under Windows (aka NTFS junction points) are not supported because java.io.File // knows nothing about them and there is no way to discriminate them. So there is no need to waste time // comparing canonical paths, just return false. // Todo: add support for .lnk files (~hard links) if(IS_WINDOWS) return false; return Files.isSymbolicLink(file.toPath()); } @Override public boolean isSystem() { if (OsFamily.MAC_OS_X.isCurrent()) { return MacOsSystemFolder.isSystemFile(this); } if (OsFamily.WINDOWS.isCurrent()) { if (!Kernel32.isAvailable()) return false; String filePath = file.getAbsolutePath(); int attributes = Kernel32.getInstance().GetFileAttributes(filePath); // if GetFileAttributes() fails we try FindFirstFile() as fallback // such a case would be pagefile.sys if(attributes == Kernel32API.INVALID_FILE_ATTRIBUTES) { Kernel32API.FindFileHandle findFileHandle = null; Kernel32API.WIN32_FIND_DATA findFileData = new Kernel32API.WIN32_FIND_DATA(); try { findFileHandle = Kernel32.getInstance().FindFirstFile(filePath, findFileData); if (findFileHandle.isValid()) { attributes = findFileData.dwFileAttributes; } } finally { if (findFileHandle != null && findFileHandle.isValid()) { Kernel32.getInstance().FindClose(findFileHandle); } } } return (attributes & Kernel32API.FILE_ATTRIBUTE_SYSTEM) != 0; } return false; } @Override public long getDate() { return file.lastModified(); } @Override public void changeDate(long lastModified) throws IOException { // java.io.File#setLastModified(long) throws an IllegalArgumentException if time is negative. // If specified time is negative, set it to 0 (01/01/1970). if(lastModified < 0) lastModified = 0; if(!file.setLastModified(lastModified)) throw new IOException(); } @Override public long getSize() { return file.length(); } @Override public AbstractFile getParent() { // Retrieve the parent AbstractFile instance and cache it if (!parentValueSet) { if(!isRoot()) { FileURL parentURL = getURL().getParent(); if(parentURL != null) { parent = FileFactory.getFile(parentURL); } } parentValueSet = true; } return parent; } @Override public void setParent(AbstractFile parent) { this.parent = parent; this.parentValueSet = true; } @Override public boolean exists() { return file.exists(); } @Override public FilePermissions getPermissions() { return permissions; } @Override public PermissionBits getChangeablePermissions() { return CHANGEABLE_PERMISSIONS; } @Override public void changePermission(PermissionAccess access, PermissionType permission, boolean enabled) throws IOException { // Only the 'user' permissions under Java 1.6 are supported if(access!=PermissionAccess.USER || JavaVersion.JAVA_1_6.isCurrentLower()) throw new IOException(); boolean success = false; switch(permission) { case READ: success = file.setReadable(enabled); break; case WRITE: success = file.setWritable(enabled); break; case EXECUTE: success = file.setExecutable(enabled); } if(!success) throw new IOException(); } /** * Always returns <code>null</code>, this information is not available unfortunately. */ @Override public String getOwner() { return null; } /** * Always returns <code>false</code>, this information is not available unfortunately. */ @Override public boolean canGetOwner() { return false; } /** * Always returns <code>null</code>, this information is not available unfortunately. */ @Override public String getGroup() { return null; } /** * Always returns <code>false</code>, this information is not available unfortunately. */ @Override public boolean canGetGroup() { return false; } @Override public boolean isDirectory() { // This test is not necessary anymore now that 'No disk' error dialogs are disabled entirely (using Kernel32 // DLL's SetErrorMode function). Leaving this code commented for a while in case the problem comes back. // // To avoid drive seeks and potential 'floppy drive not available' dialog under Win32 // // triggered by java.io.File.isDirectory() // if(IS_WINDOWS && guessFloppyDrive()) // return true; return file.isDirectory(); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public InputStream getInputStream() throws IOException { return new LocalInputStream(new FileInputStream(file).getChannel()); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public OutputStream getOutputStream() throws IOException { return new LocalOutputStream(new FileOutputStream(absPath, false).getChannel()); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public OutputStream getAppendOutputStream() throws IOException { return new LocalOutputStream(new FileOutputStream(absPath, true).getChannel()); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public RandomAccessInputStream getRandomAccessInputStream() throws IOException { return new LocalRandomAccessInputStream(new RandomAccessFile(file, "r").getChannel()); } /** * Implementation notes: the returned <code>InputStream</code> uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. */ @Override public RandomAccessOutputStream getRandomAccessOutputStream() throws IOException { return new LocalRandomAccessOutputStream(new RandomAccessFile(file, "rw").getChannel()); } @Override public void delete() throws IOException { boolean ret = file.delete(); if(!ret) throw new IOException(); } @Override public AbstractFile[] ls() throws IOException { return ls((FilenameFilter)null); } @Override public void mkdir() throws IOException { if(!file.mkdir()) throw new IOException(); } @Override public void renameTo(AbstractFile destFile) throws IOException, UnsupportedFileOperationException { // Throw an exception if the file cannot be renamed to the specified destination. // Fail in some situations where java.io.File#renameTo() doesn't. // Note that java.io.File#renameTo()'s implementation is system-dependant, so it's always a good idea to // perform all those checks even if some are not necessary on this or that platform. checkRenamePrerequisites(destFile, true, false); // The behavior of java.io.File#renameTo() when the destination file already exists is not consistent // across platforms: // - Under UNIX, it succeeds and return true // - Under Windows, it fails and return false // This ticket goes in great details about the issue: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4017593 // // => Since this method is required to succeed when the destination file exists, the Windows platform needs // special treatment. destFile = destFile.getTopAncestor(); File destJavaIoFile = ((LocalFile)destFile).file; if(IS_WINDOWS) { // This check is necessary under Windows because java.io.File#renameTo(java.io.File) does not return false // if the destination file is located on a different drive, contrary for example to Mac OS X where renameTo // returns false in this case. // Not doing this under Windows would mean files would get moved between drives with renameTo, which doesn't // allow the transfer to be monitored. // Note that Windows UNC paths are handled by checkRenamePrerequisites() when comparing hosts for equality. if(!getRoot().equals(destFile.getRoot())) throw new IOException(); // Windows 9x or Windows Me: Kernel32's MoveFileEx function is NOT available if(OsVersion.WINDOWS_ME.isCurrentOrLower()) { // The destination file is deleted before calling java.io.File#renameTo(). // Note that in this case, the atomicity of this method is not guaranteed anymore -- if // java.io.File#renameTo() fails (for whatever reason), the destination file is deleted anyway. if(destFile.exists()) if(!destJavaIoFile.delete()) throw new IOException(); } // Windows NT: Kernel32's MoveFileEx can be used, if the Kernel32 DLL is available. else if(Kernel32.isAvailable()) { // Note: MoveFileEx is always used, even if the destination file does not exist, to avoid having to // call #exists() on the destination file which has a cost. if(!Kernel32.getInstance().MoveFileEx(absPath, destFile.getAbsolutePath(), Kernel32API.MOVEFILE_REPLACE_EXISTING|Kernel32API.MOVEFILE_WRITE_THROUGH)) { String errorMessage = Integer.toString(Kernel32.getInstance().GetLastError()); // TODO: use Kernel32.FormatMessage throw new IOException("Rename using Kernel32 API failed: " + errorMessage); } else { // move successful return; } } // else fall back to java.io.File#renameTo } if(!file.renameTo(destJavaIoFile)) throw new IOException(); } @Override public long getFreeSpace() throws IOException { if(JavaVersion.JAVA_1_6.isCurrentOrHigher()) return file.getUsableSpace(); return getVolumeInfo()[1]; } @Override public long getTotalSpace() throws IOException { if(JavaVersion.JAVA_1_6.isCurrentOrHigher()) return file.getTotalSpace(); return getVolumeInfo()[0]; } // Unsupported file operations /** * Always throws {@link UnsupportedFileOperationException} when called. * * @throws UnsupportedFileOperationException, always */ @Override @UnsupportedFileOperation public void copyRemotelyTo(AbstractFile destFile) throws UnsupportedFileOperationException { throw new UnsupportedFileOperationException(FileOperation.COPY_REMOTELY); } //////////////////////// // Overridden methods // //////////////////////// @Override public String getName() { // If this file has no parent, return: // - the drive's name under OSes with root drives such as Windows, e.g. "C:" // - "/" under Unix-based systems if(isRoot()) return hasRootDrives()?absPath:"/"; return file.getName(); } @Override public String getAbsolutePath() { // Append separator for root folders (C:\ , /) and for directories if(isRoot() || (isDirectory() && !absPath.endsWith(SEPARATOR))) return absPath+SEPARATOR; return absPath; } @Override public String getCanonicalPath() { // This test is not necessary anymore now that 'No disk' error dialogs are disabled entirely (using Kernel32 // DLL's SetErrorMode function). Leaving this code commented for a while in case the problem comes back. // // To avoid drive seeks and potential 'floppy drive not available' dialog under Win32 // // triggered by java.io.File.getCanonicalPath() // if(IS_WINDOWS && guessFloppyDrive()) // return absPath; // Note: canonical path must not be cached as its resolution can change over time, for instance // if a file 'Test' is renamed to 'test' in the same folder, its canonical path would still be 'Test' // if it was resolved prior to the renaming and thus be recognized as a symbolic link try { String canonicalPath = file.getCanonicalPath(); // Append separator for directories if(isDirectory() && !canonicalPath.endsWith(SEPARATOR)) canonicalPath = canonicalPath + SEPARATOR; return canonicalPath; } catch(IOException e) { return absPath; } } @Override public String getSeparator() { return SEPARATOR; } @Override public AbstractFile[] ls(FilenameFilter filenameFilter) throws IOException { File files[] = file.listFiles(filenameFilter==null?null:new LocalFilenameFilter(filenameFilter)); if(files==null) throw new IOException(); int nbFiles = files.length; AbstractFile children[] = new AbstractFile[nbFiles]; FileURL childURL; for(int i=0; i<nbFiles; i++) { // Clone the FileURL of this file and set the child's path, this is more efficient than creating a new // FileURL instance from scratch. childURL = (FileURL)fileURL.clone(); childURL.setPath(absPath+SEPARATOR+files[i].getName()); // Retrieves an AbstractFile (LocalFile or AbstractArchiveFile) instance that's potentially already in // the cache, reuse this file as the file's parent, and the already-created java.io.File instance. children[i] = FileFactory.getFile(childURL, this, files[i]); } return children; } @Override public boolean isHidden() { return file.isHidden(); } @Override public boolean canRead() { return file.canRead(); } /** * Overridden to play nice with platforms that have root drives -- for those, the drive's root (e.g. <code>C:\</code>) * is returned instead of <code>/</code>. */ @Override public AbstractFile getRoot() { if(USES_ROOT_DRIVES) { Matcher matcher = DRIVE_ROOT_PATTERN.matcher(absPath+SEPARATOR); // Test if this file already is the root folder if(matcher.matches()) return this; // Extract the drive from the path matcher.reset(); if(matcher.find()) return FileFactory.getFile(matcher.group()); } return super.getRoot(); } /** * Overridden to play nice with platforms that have root drives -- for those, <code>true</code> is returned if * this file's path matches the drive root's (e.g. <code>C:\</code>). */ @Override public boolean isRoot() { if(USES_ROOT_DRIVES) return DRIVE_ROOT_PATTERN.matcher(absPath+SEPARATOR).matches(); return super.isRoot(); } /** * Overridden to return the local volume on which this file is located. The returned volume is one of the volumes * returned by {@link #getVolumes()}. */ @Override public AbstractFile getVolume() { AbstractFile[] volumes = LocalFile.getVolumes(); // Looks for the volume that best matches this file, i.e. the volume that is the deepest parent of this file. // If this file is itself a volume, return it. int bestDepth = -1; int bestMatch = -1; int depth; AbstractFile volume; String volumePath; String thisPath = getAbsolutePath(true); for(int i=0; i<volumes.length; i++) { volume = volumes[i]; volumePath = volume.getAbsolutePath(true); if(thisPath.equals(volumePath)) { return this; } else if(thisPath.startsWith(volumePath)) { depth = PathUtils.getDepth(volumePath, volume.getSeparator()); if(depth>bestDepth) { bestDepth = depth; bestMatch = i; } } } if(bestMatch!=-1) return volumes[bestMatch]; // If no volume matched this file (shouldn't normally happen), return the root folder return getRoot(); } /////////////////// // Inner classes // /////////////////// /** * LocalRandomAccessInputStream extends RandomAccessInputStream to provide random read access to a LocalFile. * This implementation uses a NIO <code>FileChannel</code> under the hood to benefit from * <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted using * <code>Thread#interrupt()</code>. */ public static class LocalRandomAccessInputStream extends RandomAccessInputStream { private final FileChannel channel; private final ByteBuffer bb; public LocalRandomAccessInputStream(FileChannel channel) { this.channel = channel; this.bb = BufferPool.getByteBuffer(); } @Override public int read() throws IOException { synchronized(bb) { bb.position(0); bb.limit(1); int nbRead = channel.read(bb); if(nbRead<=0) return nbRead; return 0xFF&bb.get(0); } } @Override public int read(byte b[], int off, int len) throws IOException { synchronized(bb) { bb.position(0); bb.limit(Math.min(bb.capacity(), len)); int nbRead = channel.read(bb); if(nbRead<=0) return nbRead; bb.position(0); bb.get(b, off, nbRead); return nbRead; } } @Override public void close() throws IOException { BufferPool.releaseByteBuffer(bb); channel.close(); } public long getOffset() throws IOException { return channel.position(); } public long getLength() throws IOException { return channel.size(); } public void seek(long offset) throws IOException { channel.position(offset); } } /** * A replacement for <code>java.io.FileInputStream</code> that uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. * * <p>This class simply delegates all its methods to a * {@link com.mucommander.commons.file.protocol.local.LocalFile.LocalRandomAccessInputStream} instance. Therefore, this class * does not derive from {@link com.mucommander.commons.io.RandomAccessInputStream}, preventing random-access methods from * being used.</p> * */ public static class LocalInputStream extends FilterInputStream { public LocalInputStream(FileChannel channel) { super(new LocalRandomAccessInputStream(channel)); } } /** * A replacement for <code>java.io.FileOutputStream</code> that uses a NIO {@link FileChannel} under the hood to * benefit from <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted * using <code>Thread#interrupt()</code>. * * <p>This class simply delegates all its methods to a * {@link com.mucommander.commons.file.protocol.local.LocalFile.LocalRandomAccessOutputStream} instance. Therefore, this class * does not derive from {@link com.mucommander.commons.io.RandomAccessOutputStream}, preventing random-access methods from * being used.</p> * */ public static class LocalOutputStream extends FilteredOutputStream { public LocalOutputStream(FileChannel channel) { super(new LocalRandomAccessOutputStream(channel)); } } /** * LocalRandomAccessOutputStream extends RandomAccessOutputStream to provide random write access to a LocalFile. * This implementation uses a NIO <code>FileChannel</code> under the hood to benefit from * <code>InterruptibleChannel</code> and allow a thread waiting for an I/O to be gracefully interrupted using * <code>Thread#interrupt()</code>. */ public static class LocalRandomAccessOutputStream extends RandomAccessOutputStream { private final FileChannel channel; private final ByteBuffer bb; public LocalRandomAccessOutputStream(FileChannel channel) { this.channel = channel; this.bb = BufferPool.getByteBuffer(); } @Override public void write(int i) throws IOException { synchronized(bb) { bb.position(0); bb.limit(1); bb.put((byte)i); bb.position(0); channel.write(bb); } } @Override public void write(byte b[]) throws IOException { write(b, 0, b.length); } @Override public void write(byte b[], int off, int len) throws IOException { int nbToWrite; synchronized(bb) { do { bb.position(0); nbToWrite = Math.min(bb.capacity(), len); bb.limit(nbToWrite); bb.put(b, off, nbToWrite); bb.position(0); nbToWrite = channel.write(bb); len -= nbToWrite; off += nbToWrite; } while(len>0); } } @Override public void setLength(long newLength) throws IOException { long currentLength = getLength(); if(newLength==currentLength) return; long currentPos = channel.position(); if(newLength<currentLength) { // Truncate the file and position the offset to the new EOF if it was beyond channel.truncate(newLength); if(currentPos>newLength) channel.position(newLength); } else { // Expand the file by positionning the offset at the new EOF and writing a byte, and reposition the // offset to where it was channel.position(newLength-1); // Note: newLength cannot be 0 write(0); channel.position(currentPos); } } @Override public void close() throws IOException { BufferPool.releaseByteBuffer(bb); channel.close(); } public long getOffset() throws IOException { return channel.position(); } public long getLength() throws IOException { return channel.size(); } public void seek(long offset) throws IOException { channel.position(offset); } } /** * A Permissions implementation for LocalFile. */ private static class LocalFilePermissions extends IndividualPermissionBits implements FilePermissions { private java.io.File file; // Permissions are limited to the user access type. Executable permission flag is only available under Java 1.6 // and up. // Note: 'read' and 'execute' permissions have no meaning under Windows (files are either read-only or // read-write), but we return default values. /** Mask for supported permissions under Java 1.6 */ private static PermissionBits JAVA_1_6_PERMISSIONS = new GroupedPermissionBits(448); // rwx------ (700 octal) /** Mask for supported permissions under Java 1.5 */ private static PermissionBits JAVA_1_5_PERMISSIONS = new GroupedPermissionBits(384); // rw------- (300 octal) private final static PermissionBits MASK = JavaVersion.JAVA_1_6.isCurrentOrHigher() ?JAVA_1_6_PERMISSIONS :JAVA_1_5_PERMISSIONS; private LocalFilePermissions(java.io.File file) { this.file = file; } public boolean getBitValue(PermissionAccess access, PermissionType type) { // Only the 'user' permissions are supported if(access!=PermissionAccess.USER) return false; switch(type) { case READ: return file.canRead(); case WRITE: return file.canWrite(); case EXECUTE: // Execute permission can only be retrieved under Java 1.6 and up if (JavaVersion.JAVA_1_6.isCurrentOrHigher()) return file.canExecute(); default: return false; } } /** * Overridden for performance reasons. */ @Override public int getIntValue() { int userPerms = 0; if(getBitValue(PermissionAccess.USER, PermissionType.READ)) userPerms |= PermissionType.READ.toInt(); if(getBitValue(PermissionAccess.USER, PermissionType.WRITE)) userPerms |= PermissionType.WRITE.toInt(); if(getBitValue(PermissionAccess.USER, PermissionType.EXECUTE)) userPerms |= PermissionType.EXECUTE.toInt(); return userPerms<<6; } public PermissionBits getMask() { return MASK; } } /** * Turns a {@link FilenameFilter} into a {@link java.io.FilenameFilter}. */ private static class LocalFilenameFilter implements java.io.FilenameFilter { private FilenameFilter filter; private LocalFilenameFilter(FilenameFilter filter) { this.filter = filter; } /////////////////////////////////////////// // java.io.FilenameFilter implementation // /////////////////////////////////////////// public boolean accept(File dir, String name) { return filter.accept(name); } } }